package meta

import (
	"proxy"
	"proxy/api"
	"sync"
	"time"
)

type DatabaseEventHandler interface {
	OnBackendDeleted(backendId int)
}

type Database struct {
	lastBackendId int
	backendMu     sync.Mutex
	backendById   map[int]*Backend
	confMu        sync.Mutex
	confById      map[string]*Conf
	done          chan struct{}
	handler       DatabaseEventHandler
}

func NewDatabase() *Database {
	obj := &Database{
		backendById: make(map[int]*Backend),
		confById:    make(map[string]*Conf),
		done:        make(chan struct{}),
	}

	return obj
}

func (d *Database) NewBackend(url string) (*Backend, error) {

	d.backendMu.Lock()

	if d.isTooManyBackend() {
		d.backendMu.Unlock()
		return nil, proxy.ErrNotAvail
	}

	id := d.increaseBackendId()

	if _, isDup := d.backendById[id]; isDup {
		for isDup {
			id = d.increaseBackendId()
			_, isDup = d.backendById[id]
		}
	}

	backend := NewBackend(id, url)

	d.backendById[id] = backend

	log.Info("registered backends:")
	for bid := range d.backendById {
		log.Infof("backend %d", bid)
	}

	d.backendMu.Unlock()

	return backend, nil
}

func (d *Database) SetEventHandler(handler DatabaseEventHandler) {
	d.handler = handler
}

func (d *Database) increaseBackendId() int {
	d.lastBackendId = (d.lastBackendId + 1) % kBackendIdMax
	return d.lastBackendId
}

func (d *Database) isTooManyBackend() bool {
	return len(d.backendById) > kBackendIdMax
}

// todo: Backend改成一直在超时，利用websocket的ping超时
func (d *Database) SetBackendTimeout(id int, timeout time.Duration) *Backend {
	d.backendMu.Lock()
	b, ok := d.backendById[id]
	if ok {
		b.expireTime = time.Now().Add(timeout)
	}
	d.backendMu.Unlock()
	return b
}

func (d *Database) Close() {
	close(d.done)
}

func (d *Database) Run() {
	timer := time.NewTicker(kExpireCheckInterval)
	for {
		select {
		case <-d.done:
			log.Warning("database done")
			return
		case now := <-timer.C:
			d.deleteTimeoutBackendAndConf(now)
			//d.dumpBackends()
		}
	}
}

func (d *Database) deleteTimeoutBackendAndConf(now time.Time) {

	d.backendMu.Lock()

eachBackend:
	for backendId, backend := range d.backendById {
		if backend.IsExpired() {
			if d.handler != nil {
				d.handler.OnBackendDeleted(backendId)
			}
			log.Warningf("backend '%d' timeout", backendId)
			delete(d.backendById, backendId)
			continue eachBackend
		}
		backend.HandleRtcTimeout()
	}

	d.backendMu.Unlock()

	d.confMu.Lock()

eachConf:
	for confId, conf := range d.confById {
		if conf.IsExpired(now) {
			log.Warningf("conf '%s' timeout", confId)
			delete(d.confById, confId)
			continue
		}
		conf.HandleRtcTimeout()
		continue eachConf
	}

	d.confMu.Unlock()
}

func (d *Database) Status() *api.MetaStatus {
	status := &api.MetaStatus{
		BackendMap: make(map[int]*api.BackendStatus),
		ConfMap:    make(map[string]*api.ConfStatus),
	}

	d.backendMu.Lock()
	for _, backend := range d.backendById {
		backendStatus := backend.Status()
		status.BackendMap[backend.Id] = backendStatus

	}
	d.backendMu.Unlock()

	d.confMu.Lock()
	for _, conf := range d.confById {
		confStatus := conf.Status()
		status.ConfMap[conf.Id] = confStatus
	}
	d.confMu.Unlock()

	return status
}

func (d *Database) GetBackend(backendId int) *Backend {
	d.backendMu.Lock()
	backend := d.backendById[backendId]
	d.backendMu.Unlock()
	return backend
}

func (d *Database) ReNewBackend(backendId int, backendUrl string) *Backend {
	d.backendMu.Lock()

	if d.isTooManyBackend() {
		log.Warningf("reconnect backend %d error: %d backends registered, too many", backendId, len(d.backendById))
		d.backendMu.Unlock()
		return nil
	}

	b, ok := d.backendById[backendId]
	if ok {
		if b.url != backendUrl {
			log.Warningf("found reconnect backend %d error: url not match", backendId, backendUrl)
			d.backendMu.Unlock()
			return nil
		}
		log.Warningf("found reconnect backend %d", backendId)
		d.backendMu.Unlock()
		return b
	}

	log.Warningf("could not find reconnect backend %d, create it", backendId)

	backend := NewBackend(backendId, backendUrl)

	d.backendById[backendId] = backend

	d.backendMu.Unlock()

	return backend
}

func (d *Database) NewConf(confId string, kind string) (*Conf, error) {
	d.confMu.Lock()

	conf, isConfDup := d.confById[confId]

	if isConfDup {
		d.confMu.Unlock()
		return conf, nil
	}

	conf = NewConf()

	conf.Id = confId

	conf.Kind = kind

	d.confById[confId] = conf

	d.confMu.Unlock()

	return conf, nil
}

func (d *Database) GetConf(confId string) *Conf {
	d.confMu.Lock()

	conf, ok := d.confById[confId]

	if !ok {
		d.confMu.Unlock()
		return nil
	}

	d.confMu.Unlock()
	return conf
}

func (d *Database) DelConf(confId string) {
	d.confMu.Lock()
	delete(d.confById, confId)
	d.confMu.Unlock()
}

func (d *Database) KeepAliveConf(now time.Time, confId string, confKind string, rtcIdList []string) {
	d.confMu.Lock()

	conf, ok := d.confById[confId]

	if !ok {
		conf = NewConf()
		conf.Id = confId
		conf.Kind = confKind
		d.confById[confId] = conf
	}

	conf.KeepAlive(now, rtcIdList)

	d.confMu.Unlock()
}

func (d *Database) BackendAll() []*Backend {
	backends := make([]*Backend, 0)

	d.backendMu.Lock()
	for _, backend := range d.backendById {
		backends = append(backends, backend)
	}
	d.backendMu.Unlock()

	return backends
}
